/*
 * @(#)JarDiffHandler.java 1.9 05/11/30 Copyright (c) 2006 Sun Microsystems,
 * Inc. All Rights Reserved. Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided that the following
 * conditions are met: -Redistribution of source code must retain the above
 * copyright notice, this list of conditions and the following disclaimer.
 * -Redistribution in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution. Neither the name of
 * Sun Microsystems, Inc. or the names of contributors may be used to endorse or
 * promote products derived from this software without specific prior written
 * permission. This software is provided "AS IS," without a warranty of any
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MIDROSYSTEMS, INC.
 * ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY
 * LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
 * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. You
 * acknowledge that this software is not designed, licensed or intended for use
 * in the design, construction, operation or maintenance of any nuclear
 * facility.
 */

package jnlp.sample.servlet;

import java.io.*;
import java.util.*;
import jnlp.sample.jardiff.*;
import javax.servlet.*;
import javax.servlet.http.*;
import jnlp.sample.util.VersionString;
import java.net.URL;

/*
 * A class that generates and caches information about JarDiff files
 */
public class JarDiffHandler
{
    // Default size of download buffer
    private static final int BUF_SIZE = 32 * 1024;

    // Default JARDiff mime type
    private static final String JARDIFF_MIMETYPE = "application/x-java-archive-diff";

    /** List of all generated JARDiffs */
    private HashMap _jarDiffEntries = null;

    /** Reference to ServletContext and logger object */
    private static Logger _log = null;

    private ServletContext _servletContext = null;

    private String _jarDiffMimeType = null;

    /* Contains information about a particular JARDiff entry */
    private static class JarDiffKey implements Comparable
    {
	private String _name; // Name of file

	private String _fromVersionId; // From version

	private String _toVersionId; // To version

	private boolean _minimal; // True if this is a minimal jardiff

	/** Constructor used to generate a query object */
	public JarDiffKey(String name, String fromVersionId, String toVersionId, boolean minimal)
	{
	    _name = name;
	    _fromVersionId = fromVersionId;
	    _toVersionId = toVersionId;
	    _minimal = minimal;
	}

	// Query methods
	public String getName()
	{
	    return _name;
	}

	public String getFromVersionId()
	{
	    return _fromVersionId;
	}

	public String getToVersionId()
	{
	    return _toVersionId;
	}

	public boolean isMinimal()
	{
	    return _minimal;
	}

	// Collection framework interface methods

	public int compareTo(Object o)
	{
	    // All non JarDiff entries are less
	    if (!(o instanceof JarDiffKey))
		return -1;
	    JarDiffKey other = (JarDiffKey) o;

	    int n = _name.compareTo(other.getName());
	    if (n != 0)
		return n;

	    n = _fromVersionId.compareTo(other.getFromVersionId());
	    if (n != 0)
		return n;

	    if (_minimal != other.isMinimal())
		return -1;

	    return _toVersionId.compareTo(other.getToVersionId());
	}

	public boolean equals(Object o)
	{
	    return compareTo(o) == 0;
	}

	public int hashCode()
	{
	    return _name.hashCode() + _fromVersionId.hashCode() + _toVersionId.hashCode();
	}
    }

    static private class JarDiffEntry
    {
	private File _jardiffFile; // Location of JARDiff file

	public JarDiffEntry(File jarDiffFile)
	{
	    _jardiffFile = jarDiffFile;
	}

	public File getJarDiffFile()
	{
	    return _jardiffFile;
	}
    }

    /** Initialize JarDiff handler */
    public JarDiffHandler(ServletContext servletContext, Logger log)
    {
	_jarDiffEntries = new HashMap();
	_servletContext = servletContext;
	_log = log;

	_jarDiffMimeType = _servletContext.getMimeType("xyz.jardiff");
	if (_jarDiffMimeType == null)
	    _jarDiffMimeType = JARDIFF_MIMETYPE;
    }

    /** Returns a JarDiff for the given request */
    public synchronized DownloadResponse getJarDiffEntry(ResourceCatalog catalog,
	    DownloadRequest dreq, JnlpResource res)
    {
	if (dreq.getCurrentVersionId() == null)
	    return null;

	// check whether the request is from javaws 1.0/1.0.1
	// do not generate minimal jardiff if it is from 1.0/1.0.1
	boolean doJarDiffWorkAround = isJavawsVersion(dreq, "1.0*");

	// First do a lookup to find a match
	JarDiffKey key = new JarDiffKey(res.getName(), dreq.getCurrentVersionId(), res
		.getReturnVersionId(), !doJarDiffWorkAround);

	JarDiffEntry entry = (JarDiffEntry) _jarDiffEntries.get(key);
	// If entry is not found, then the querty has not been made.
	if (entry == null)
	{
	    if (_log.isInformationalLevel())
	    {
		_log.addInformational("servlet.log.info.jardiff.gen", res.getName(), dreq
			.getCurrentVersionId(), res.getReturnVersionId());
	    }
	    File f = generateJarDiff(catalog, dreq, res, doJarDiffWorkAround);
	    if (f == null)
	    {
		_log.addWarning("servlet.log.warning.jardiff.failed", res.getName(), dreq
			.getCurrentVersionId(), res.getReturnVersionId());
	    }
	    // Store entry in table
	    entry = new JarDiffEntry(f);
	    _jarDiffEntries.put(key, entry);
	}

	// Check for no JarDiff to return
	if (entry.getJarDiffFile() == null)
	{
	    return null;
	}
	else
	{
	    return DownloadResponse.getFileDownloadResponse(entry.getJarDiffFile(),
		    _jarDiffMimeType, entry.getJarDiffFile().lastModified(), res
			    .getReturnVersionId());
	}
    }

    public static boolean isJavawsVersion(DownloadRequest dreq, String version)
    {
	String javawsAgent = "javaws";
	String jwsVer = dreq.getHttpRequest().getHeader("User-Agent");

	// check the request is coming from javaws
	if (!jwsVer.startsWith("javaws-"))
	{
	    // this is the new style User-Agent string
	    // User-Agent: JNLP/1.0.1 javaws/1.4.2 (b28) J2SE/1.4.2
	    StringTokenizer st = new StringTokenizer(jwsVer);
	    while (st.hasMoreTokens())
	    {
		String verString = st.nextToken();
		int index = verString.indexOf(javawsAgent);
		if (index != -1)
		{
		    verString = verString.substring(index + javawsAgent.length() + 1);
		    return VersionString.contains(version, verString);
		}
	    }
	    return false;
	}

	// extract the version id from the download request
	int startIndex = jwsVer.indexOf("-");

	if (startIndex == -1)
	{
	    return false;
	}

	int endIndex = jwsVer.indexOf("/");

	if (endIndex == -1 || endIndex < startIndex)
	{
	    return false;
	}

	String verId = jwsVer.substring(startIndex + 1, endIndex);

	// check whether the versionString contains the versionId
	return VersionString.contains(version, verId);

    }

    /** Download resource to the given file */
    private boolean download(URL target, File file)
    {

	_log.addDebug("JarDiffHandler:  Doing download");

	boolean ret = true;
	boolean delete = false;
	// use bufferedstream for better performance
	BufferedInputStream in = null;
	BufferedOutputStream out = null;
	try
	{
	    in = new BufferedInputStream(target.openStream());
	    out = new BufferedOutputStream(new FileOutputStream(file));
	    int read = 0;
	    int totalRead = 0;
	    byte[] buf = new byte[BUF_SIZE];
	    while ((read = in.read(buf)) != -1)
	    {
		out.write(buf, 0, read);
		totalRead += read;
	    }

	    _log.addDebug("total read: " + totalRead);
	    _log.addDebug("Wrote URL " + target.toString() + " to file " + file);

	}
	catch (IOException ioe)
	{

	    _log.addDebug("Got exception while downloading resource: " + ioe);

	    ret = false;

	    if (file != null)
		delete = true;

	}
	finally
	{

	    try
	    {
		in.close();
		in = null;
	    }
	    catch (IOException ioe)
	    {
		_log.addDebug("Got exception while downloading resource: " + ioe);
	    }

	    try
	    {
		out.close();
		out = null;
	    }
	    catch (IOException ioe)
	    {
		_log.addDebug("Got exception while downloading resource: " + ioe);
	    }

	    if (delete)
	    {
		file.delete();
	    }

	}
	return ret;
    }

    // fix for 4720897
    // if the jar file resides in a war file, download it to a temp dir
    // so it can be used to generate jardiff
    private String getRealPath(String path) throws IOException
    {

	URL fileURL = _servletContext.getResource(path);

	File tempDir = (File) _servletContext.getAttribute("javax.servlet.context.tempdir");

	// download file into temp dir
	if (fileURL != null)
	{
	    File newFile = File.createTempFile("temp", ".jar", tempDir);
	    if (download(fileURL, newFile))
	    {
		String filePath = newFile.getPath();
		return filePath;
	    }
	}
	return null;
    }

    private File generateJarDiff(ResourceCatalog catalog, DownloadRequest dreq, JnlpResource res,
	    boolean doJarDiffWorkAround)
    {
	boolean del_old = false;
	boolean del_new = false;

	// Lookup up file for request version
	DownloadRequest fromDreq = dreq.getFromDownloadRequest();
	try
	{
	    JnlpResource fromRes = catalog.lookupResource(fromDreq);

	    /* Get file locations */
	    String newFilePath = _servletContext.getRealPath(res.getPath());
	    String oldFilePath = _servletContext.getRealPath(fromRes.getPath());

	    // fix for 4720897
	    if (newFilePath == null)
	    {
		newFilePath = getRealPath(res.getPath());
		if (newFilePath != null)
		    del_new = true;
	    }

	    if (oldFilePath == null)
	    {
		oldFilePath = getRealPath(fromRes.getPath());
		if (oldFilePath != null)
		    del_old = true;
	    }

	    if (newFilePath == null || oldFilePath == null)
	    {
		return null;
	    }

	    // Create temp. file to store JarDiff file in
	    File tempDir = (File) _servletContext.getAttribute("javax.servlet.context.tempdir");

	    // fix for 4653036: JarDiffHandler() should use
	    // javax.servlet.context.tempdir to store the jardiff
	    File outputFile = File.createTempFile("jnlp", ".jardiff", tempDir);

	    _log.addDebug("Generating Jardiff between " + oldFilePath + " and " + newFilePath
		    + " Store in " + outputFile);

	    // Generate JarDiff
	    OutputStream os = new FileOutputStream(outputFile);

	    JarDiff.createPatch(oldFilePath, newFilePath, os, !doJarDiffWorkAround);
	    os.close();

	    try
	    {

		// Check that Jardiff is smaller, or return null
		if (outputFile.length() >= (new File(newFilePath).length()))
		{
		    _log.addDebug("JarDiff discarded - since it is bigger");
		    return null;
		}

		// Check that Jardiff is smaller than the packed version of
		// the new file, if the file exists at all
		File newFilePacked = new File(newFilePath + ".pack.gz");
		if (newFilePacked.exists())
		{
		    _log.addDebug("generated jardiff size: " + outputFile.length());
		    _log.addDebug("packed requesting file size: " + newFilePacked.length());
		    if (outputFile.length() >= newFilePacked.length())
		    {
			_log
				.addDebug("JarDiff discarded - packed version of requesting file is smaller");
			return null;
		    }
		}

		_log.addDebug("JarDiff generation succeeded");
		return outputFile;

	    }
	    finally
	    {
		// delete the temporarily downloaded file
		if (del_new)
		{
		    new File(newFilePath).delete();
		}

		if (del_old)
		{
		    new File(oldFilePath).delete();
		}
	    }
	}
	catch (IOException ioe)
	{
	    _log.addDebug("Failed to genereate jardiff", ioe);
	    return null;
	}
	catch (ErrorResponseException ere)
	{
	    _log.addDebug("Failed to genereate jardiff", ere);
	    return null;
	}
    }
}
